package BNS.wheat;

import java.io.*;
import java.rmi.*;
import java.rmi.server.UnicastRemoteObject;
import java.lang.Thread;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.util.*;
import BNS.graphs.*;
import BNS.plumbing.*;
/* TODO: Should the graph itself designate the commodity of the node? 
		Present policy assumes the low-numbered nodes are one thing, and highs are another.
		Does that jive with how the graphs are created??
*/

/** This class implements the Wheat Market game.
	Each player starts with an endowment of goods s/he does not value;
	the payoff for each player is proportional to the amount of goods s/he can purchase from  
	neighbours. Since everyone does value what hisr neighbours have, trades are the rational
	tactic. A mechanism is provided for advertising prices, and for executing trades when
	bilateral agreements on price are reached. We anticipate that variation in the nature of
	local neighbourhoods will manifest itself in differences in prices and acquisitions.
	
	<p>The counterpart class in the client machine is a WheatMktPersona.
	*/
public class WheatMktGame extends BNSGameController {
	
	protected NodeState[] nodeStates;	// stuff pertaining to nodes, indexed by Node.getIndex()
	protected NodeState[][] neighbourhood;	// all the nodes in each neighbourhood
		
	protected int perfection, currentProgress;
	protected static final long serialVersionUID = 01L;
	protected int gameSize;
	protected GraphPanel gPanel;
	static final float initialEndowment= 10.0f, dollarsPerUnit= 0.20f;
	private boolean marketStuck;
	
	/** TODO: remove this; it is not used */
	public static enum Commodity {nada,wheat,sugar};
	
	/** Creates a serializable game with all parameters initialized.
		@param gp a GraphPack defining graph views for all players.
		@param time the maximum amount of game time allowed
		*/
	public WheatMktGame(GraphPack gp, int time) {
		super(time);
		grpk= gp;
		System.err.println("graph info...\n "+grpk.getInfo());
		}
	
	public void init(Manager mgr, Point locn, PrintStream sessionLog, PrintStream gameLog) {
		super.init(mgr, locn, sessionLog, gameLog);
		System.err.println("WheatMktGame.init("+mgr+","+locn+")");
		sessionLog.println(getTitle()+"  "+ new Date());
		sessionLog.println("source "+ getSource());
		sessionLog.println(grpk.getInfo());
		gameSize= grpk.graph.numNodes();
		gameValue= "unsolved";
		neighbourhood= new NodeState[gameSize][];
		nodeStates= new NodeState[gameSize];
		for (int nsi=0; nsi<gameSize; nsi++) {
			nodeStates[nsi]= new NodeState(nsi,initialEndowment);
			}
		for (int nsi=0; nsi<gameSize; nsi++) {
			NodeState nodeState= nodeStates[nsi];
			Graph graphlet= grpk.graphlets[nsi];
			neighbourhood[nsi]= new NodeState[graphlet.numNodes()];
			neighbourhood[nsi][0]= nodeStates[nsi];	// first node in array must be the eigenNode
			Enumeration ene= graphlet.enumEdges();
			int nNum= 1;
			while (ene.hasMoreElements()) {
				Edge e= (Edge)ene.nextElement();
				// discard any edges not connected to eigenNode
				// but since the graphs are all bipartite, this should never happen!
				assert(e.contains(nsi));
				int nIdx= e.to.getIndex()==nsi? e.from.getIndex() : e.to.getIndex();
				neighbourhood[nsi][nNum++]= nodeStates[nIdx];
				}
			assert (nNum==neighbourhood[nsi].length);
			nodeState.market= new Market(neighbourhood[nsi],gameLog);
			}
		
		gPanel= new WealthPanel(grpk.graph,nodeStates);
		monitor= new Monitor(gPanel,locn);
		marketStuck= false;
		System.err.println("WheatMktGame.init()ed"); 
		}
	
	public int monitorUpdate(Monitor mon) {
		if (marketStuck) mon.welfareDisplay.setText("FINISHED!");
		else mon.welfareDisplay.setText("active");
		mon.timeDisplay.setText(String.format(" %d ",clock.getElapsedTime()/1000));
		return 300;
		}
	
	public int getSize() { return grpk.size(); }
	
	public String getTitle() { return "WheatMktGame"; }
	
	public String getInfo() { 
		return getTitle() +" of size "+ getSize() +"\n"
			+" time allowed="+ timeLimit;
		}
	
	/** used to change the state of a game on behalf of players.
		The first char of tokens[4] must be a 'B' (which indicates a Buy operation).
		tokens[5] and tokens[6] are pay/get values for a proposed transaction.
		@param tstamp the elapsed time (in milliseconds) of this game so far.
		@param client representing the player making the change.
		@param tokens array holding all arguments required for the change.
		@return indicator of success at making the change. 
		It returns true iff the game is not over, the game is not paused, and the game 
		had no errors while trying to carry out the changes implied by changeObject. 
		*/
	public boolean interpret(long tstamp, ClientLiaison client, String[] tokens) {
		char type= tokens[4].charAt(0);
//		System.err.println(" did "+type+" "+tokens[5]);
		if (type=='B') trade(tstamp,client,new Market.Deal(
							Float.parseFloat(tokens[5]),Float.parseFloat(tokens[6])) );
		else System.err.println("type="+type+"??");
		return false;
		}
	
	/** used to change the state of a game on behalf of players.
		@param changeObj defines an amount and price for sale;
			must be of type Market.Deal for this game.
		@return indicates success at making the change.
		It returns true iff the game is not over, the game is not paused, and the game 
		had no errors while trying to carry out the changes implied by changeObject. 
		*/
	public boolean alter(ClientLiaison client, Serializable changeObj) {
		long tstamp= clock.getElapsedTime();
		if (changeObj instanceof Market.Deal) 
			return trade(tstamp, client,(Market.Deal)changeObj); 
		assert(false);
		return false;
		}
	
	/** This method accomplishes two separate tasks. There is no reason other than 
		computational efficiency for these two purposes to be commingled.
		The first is to find the highest bid that each player is being offered for
		his stuff. The results of this calculation are left in nodeStates[*].bestPrice .
		
		The second purpose is to test whether the market is exhausted. If there are no 
		more possible exchanges left in the network, then this returns true. Otherwise, 
		it returns false, indicating that there is reason to let the game continue.  
		*/
	private boolean cleared() {
		boolean clear= true;
		for (int i=0; i<nodeStates.length; i++) 
			nodeStates[i].bestPrice= Float.MAX_VALUE;
		Enumeration ene= grpk.graph.enumEdges();
		while (ene.hasMoreElements()) {
			Edge e= (Edge)ene.nextElement();
			int toIdx= e.to.getIndex();
			int frIdx= e.from.getIndex();
			if (nodeStates[frIdx].sellingVolume > 0)
				nodeStates[toIdx].bestPrice= Math.min(nodeStates[toIdx].bestPrice,
													  nodeStates[frIdx].sellingPrice);
			if (nodeStates[toIdx].sellingVolume > 0)
				nodeStates[frIdx].bestPrice= Math.min(nodeStates[frIdx].bestPrice,
													  nodeStates[toIdx].sellingPrice);
			if (nodeStates[frIdx].endowment > 0 && nodeStates[toIdx].endowment > 0)
				clear= false;
			}
		return clear;
		}
		
	/** posts an amount of good for sale and may cause an exchange of goods.
		See Market.trade for more details of how trades occur.
		If the game is suspended() or completed, 
		the sell order will not be posted and this routine returns false; 
		otherwise the order is posted and this routine returns true. 
		@param client the liaison object representing a client and player.
		@param deal the PAY and GET quantities involved.
		@return a boolean indicating if the sell order was accepted. 
		*/
	public synchronized boolean trade(long tstamp, ClientLiaison client, Market.Deal deal) {
		System.err.println();
		System.err.println(" client #"+client.clientID+" pay/get "+deal+" node "+client.eigenNode);
		int nodeIdx= client.eigenNode.getIndex();
		NodeState nodeState= nodeStates[nodeIdx];
		//System.err.println(" new purchase="+ deal.toString(nodeState.commodity));
		synchronized(nodeStates) {	// only this section needs to be threadsafe
			if (clock.suspended() || gameCompleted) return false;
			// TODO: worry about deal.pay being Infinity (!)
			gameLog.println("move "+tstamp+" "+client.clientID+" "+client.eigenNode
							+" B "+deal.pay+" "+deal.get);
			//gameLog.format("sell %7d %3d %s%n", 
			//				tstamp, client.eigenNode.getIndex()+1, deal.round());
			double traded= nodeState.market.trade(deal);
			System.err.println("traded volume= "+traded);
			//if (traded>0) gameLog.println(tstamp+" "+client.clientID+" "+traded);
			assert(traded >= 0);
			if (cleared() && !marketStuck) {
				gameLog.println("clrd");
				marketStuck= true;
				}
			//showTable(System.err);
			}
		monitor.repaint();
		return true;
		}
	
	/** Prints a table showing the status of every node in the network
		on a given PrintStream. It lists remainging endowment, amount acquired so far,
		and the current sales order each player has posted (price, volume). 
		This is for debugging purposes. 
		*/
	private void showTable(PrintStream output) {
		for (int i=0; i<nodeStates.length; i++) 
			output.format("%6.2f",nodeStates[i].endowment);
		output.println("  <== endowment");
		for (int i=0; i<nodeStates.length; i++) 
			output.format("%6.2f",nodeStates[i].acquired);
		output.println("  <== acquired");
		for (int i=0; i<nodeStates.length; i++) 
			if (nodeStates[i].sellingVolume > 0)
				output.format("%6.2g",nodeStates[i].sellingPrice);
			else output.print("   -  ");
		output.println("  <== sellingPrice");
		for (int i=0; i<nodeStates.length; i++) 
			if (nodeStates[i].sellingVolume > 0)
				output.format("%6.2f",nodeStates[i].sellingVolume);
			else output.print("   -  ");
		output.println("  <== sellingVolume");
		}
		
	/** called by ClientLiaison on behalf of Client to get data relevant to a node. 
		@param nodeID the identifying index of the node in the game graph.
		@return an Envelope, which WheatMktPersona must interpret as
			describing the current local state of the game in its graphlet. 
		*/
	public ClientLiaison.StatusCarrier getUpdate(int nodeID) {	// TODO: why statusCarrier?
		Envelope env= new Envelope();
		env.elapsedTime= clock.getElapsedTime();
		env.currentProgress= currentProgress;
		env.payRate= 0;
		env.cumulativePay= nodeStates[nodeID].acquired* dollarsPerUnit;
		// The nodeStates datastructure is fixed, but the data in it is updated by calls 
		// to trade(). Here we simply enclose the array that points to the data for nodeID.
		env.local= neighbourhood[nodeID];
		return env;
		}
	
	public BNSPersona getPersona(ClientLiaison client, Node node) {
		//System.err.println("creating new WheatMktPersona("+ node +") for "+ client.clientID);
		int nodeIdx= node.getIndex();
		nodeStates[nodeIdx].client= client;
		return new WheatMktPersona(client, grpk.graphlets[nodeIdx],
					neighbourhood[nodeIdx], timeLimit, perfection);
		}
	
	public float getPayoff(int nodeID, int time) {
		return nodeStates[nodeID].acquired* dollarsPerUnit; }
	
	/** Goes through all clients, calculates their final payoff, and sends a message
		to each client reporting same.	*/
	public void showPayoffs(int time) {
/*		float gamePay= 0.0f;
		gameLog.print("earn");
		for (int nsi= 0; nsi<gameSize; nsi++) {
			ClientLiaison client= nodeStates[nsi].client;
//			if (client==null || client.game!=gameController || client.empty) continue;
			float pay= getPayoff(client.eigenNode.getIndex(), time);
			gamePay+= pay;
			client.sendMessage(String.format("game OVER; your final payoff is $%.2f", pay));
			gameLog.format(" %f",pay);
			}
		gameLog.format(" %f%n",gamePay);
*/		super.showPayoffs(time);
		
		gameLog.print("held");
		for (int nsi= 0; nsi<gameSize; nsi++) {
			gameLog.format(" %f",nodeStates[nsi].endowment);
			}
		gameLog.format("%n");
		
		gameLog.print("avbl");
		for (int nsi=0; nsi<gameSize; nsi++) {
			float avbl= 0;
			for (int ni=1; ni<neighbourhood[nsi].length; ni++)	// index 0 is for eigenNode
				avbl+= neighbourhood[nsi][ni].endowment;
			gameLog.format(" %f",avbl);
			}
		gameLog.format("%n");
		
//		gameLog.print("clid");
//		for (int nsi=0; nsi<gameSize; nsi++) 
//			gameLog.print(" "+nodeStates[nsi].client.clientID);
//		gameLog.println();
		}
	
	} // EOC WheatMktGame
